package merlin.utils;

import java.io.*;

import java.net.*;

import java.text.*;

import java.util.*;

import merlin.beans.*;
import merlin.utils.ApplicationResources;


/**
 * Utility class to execute a real-time credit card transaction.
 * This class connects to the PROTX website and uses the website's
 * facilities to validate a transaction.
 *
 *
 * Change History
 * --------------
 * Who		When			What
 * ------------------------------
 * sandip	16 Oct 2006		Created
 */
public class CreditCardTransaction{

	/**
	 * Status codes int numeric format.
	 */
	public static final int STATUS_UNKNOWN 	 = 0;
	public static final int STATUS_OK 		 = "OK".hashCode();
	public static final int STATUS_MALFORMED = "MALFORMED".hashCode();
	public static final int STATUS_INVALID 	 = "INVALID".hashCode();
	public static final int STATUS_ERROR 	 = "ERROR".hashCode();
	public static final int STATUS_NOTAUTHED = "NOTAUTHED".hashCode();
	public static final int STATUS_REJECTED  = "REJECTED".hashCode();

	public static final String FORCE_3D_SECURE_AUTHENTICATION 	= "1";
	public static final String SKIP_3D_SECURE_AUTHENTICATION 	= "2";


	/**
	 * Function to execute the transaction.
	 * The function opens a HTTPS connection on the PROTX website URL,
	 * submits the purchase details, and accepts back the response
	 * that it reformats into a name-value paired Map object.
	 *
	 * @return
	 * 			A name-value formatted response data returned from the
	 * 			PROTX website.
	 * 			Note : A null object is returned in case there is some
	 * 			error in connecting to the PROTX URL or while posting data
	 * 			to it.
	 */
	public static Map doTransaction(WebsiteOrdersBean woBean,int orderId,Map mapPrices,double discountAmount){

		String[] arrRawReports = woBean.getRawReports();

		StringBuffer sbBasket = new StringBuffer();

		// The format of the Basket contents is described in the ProtX VSP documentation.

		boolean hasMerlinReports = (arrRawReports != null && arrRawReports.length > 0);
		boolean hasIWReports = woBean.isIndustryWatchReports();

		System.out.println("Merlin Reports : " + hasMerlinReports);
		System.out.println("isIndustryWatchReports " + woBean.isIndustryWatchReports());

		// Number of lines of detail in the basket field
		int totalLineItems = 0;
		if(hasMerlinReports){
			totalLineItems += (arrRawReports.length/2);
		}
		if(hasIWReports){
			totalLineItems++;
		}
		if(woBean.isPdfReportPurchase()){
			totalLineItems++;
		}
		// contains one extra line containing the total amount field.
		totalLineItems++;

		DecimalFormat formatter = new DecimalFormat("##,##0.00");

		sbBasket.append(totalLineItems).append(':');

		// if there are Merlin reports, add the report names
		if(hasMerlinReports){

			String firstNetPrice 		= (String)mapPrices.get("firstNetPrice");
			String subsequentNetPrice 	= (String)mapPrices.get("subsequentNetPrice");
			String firstVatPrice 		= (String)mapPrices.get("firstVatPrice");
			String subsequentVatPrice 	= (String)mapPrices.get("subsequentVatPrice");

			for(int i=0;i< arrRawReports.length-1;i+=2){
				//Item n Description
		  		sbBasket.append(arrRawReports[i]).append('-').append(arrRawReports[i+1]).append(':');
		  		String itemNetPrice = null, itemVatPrice = null;
		  		// applicable only for normal reports, ignore custom reports.
		  		if(! MerlinUtilities.isCustomReport(arrRawReports[i])){
			  		if(i==0){
						itemNetPrice = firstNetPrice;
						itemVatPrice = firstVatPrice;
			  		}else{
						itemNetPrice = subsequentNetPrice;
						itemVatPrice = subsequentVatPrice;
			  		}
		  		}else{
					itemNetPrice = "0";
					itemVatPrice = "0";
		  		}
				double dItemTotal = Double.parseDouble(itemNetPrice) + Double.parseDouble(itemVatPrice);
   				String itemTotal = formatter.format(dItemTotal);
   				String lineTotal = itemTotal;

				// Quantity:Unit cost: Tax applied :Cost of Item:Line total
				sbBasket.append("1:").append(itemNetPrice).append(":").append(itemVatPrice).append(":").append(itemTotal).append(":").append(lineTotal).append(":");
				System.out.println("--" + arrRawReports[i]);
		  	}
		}

		// if there are IW reports, add the name
		if(hasIWReports){
			IWRegistrationBean iwRegBean = (IWRegistrationBean)mapPrices.get("iwRegBean");

			if("A".equals(iwRegBean.getChkIndustryWatch())){
				sbBasket.append("Industry Watch Reports").append(':');
			}else{
				if("B".equals(iwRegBean.getChkIndustryWatch())){
					sbBasket.append("Industry Watch Reports - with 10 report downloads").append(':');
				}else{
					if("C".equals(iwRegBean.getChkIndustryWatch())){
						sbBasket.append("Industry Watch Reports - with 7 days subscription").append(':');
					}
				}
			}
			String netIndustryWatch = (String)mapPrices.get("netIndustryWatch");
	  		String vatIndustryWatch = (String)mapPrices.get("vatIndustryWatch");

			double dIWItemTotal = Double.parseDouble(netIndustryWatch) + Double.parseDouble(vatIndustryWatch);

			String iwItemTotal = formatter.format(dIWItemTotal);
			String iwLineTotal = iwItemTotal;
			// Quantity:Unit cost: Tax applied :Cost of Item:Total Line Item
			sbBasket.append("1:").append(netIndustryWatch).append(":").append(vatIndustryWatch).append(":").append(iwItemTotal).append(":").append(iwLineTotal).append(":");
			System.out.println("-- add IW");
		}

		// PDF report purchase
		if(woBean.isPdfReportPurchase()){
   			sbBasket.append("PDF Report Download purchase:").append(woBean.getNumPdfReports()).append(":").append(mapPrices.get("netPdfPurchase")).append(":").append(mapPrices.get("vatPdfPurchase")).append(":").append(mapPrices.get("totalPdfPurchase")).append(":").append(mapPrices.get("totalPdfPurchase")).append(":");
		}


		String formatInfo = "";

		// if there are merlin reports, add the
	  	if(hasMerlinReports){
	  		formatInfo = woBean.getFormatOptions();
		}
		if(hasIWReports){
			if(formatInfo.length() > 0)formatInfo += " and ";	// Merlin Reports are also there.
			formatInfo += "Industry Watch Reports";
		}
		// if there is a discount applied, put the info here.
		if(discountAmount > 0.0){
			formatInfo += "(discount " + formatter.format(discountAmount) + ")";
		}
	  	// Total Amount
	  	sbBasket.append(formatInfo).append(":::::").append(woBean.getTotalAmount());

		// transaction code
		String vendorTxCode = String.valueOf(orderId);

		ApplicationResources resource = ApplicationResources.getInstance();

		// the request parameters to be sent to the ProtX URL
		Map mapRequest = new HashMap();
	 	mapRequest.put("VPSProtocol"	, resource.getProperty("VPSProtocol"));
	 	mapRequest.put("TxType"		, resource.getProperty("TxType"));
	 	mapRequest.put("Vendor"		, resource.getProperty("Vendor"));
	 	mapRequest.put("VendorTxCode"	, vendorTxCode);
	 	// format the amount so that it has two decimal places.
	 	mapRequest.put("Amount"		, new java.text.DecimalFormat("#######.00").format(Double.parseDouble(woBean.getTotalAmount())));
	 	mapRequest.put("Currency"		, resource.getProperty("Currency"));
	 	mapRequest.put("Description"	, "Full Details in the Basket");

	 	StringBuffer strCardHolder 		= new StringBuffer();
		StringBuffer strBillingAddress 	= new StringBuffer();
		String strBillingPostCode 		= null;

		StringBuffer strDeliveryAddress	= new StringBuffer();
		String strDeliveryPostCode		= null;

		// assemble the delivery details.
		String temp = woBean.getDdAddressLine1();
		if(temp != null && temp.length() > 0) strDeliveryAddress.append(temp);
		temp = woBean.getDdAddressLine2();
		if(temp != null && temp.length() > 0) strDeliveryAddress.append(',').append(temp);
		temp = woBean.getDdTown();
		if(temp != null && temp.length() > 0) strDeliveryAddress.append(',').append(temp);
		temp = woBean.getDdCounty();
		if(temp != null && temp.length() > 0) strDeliveryAddress.append(',').append(temp);

		strDeliveryPostCode = woBean.getDdPostcode();

		// assemble the credit card billing address & postcode
		if(woBean.isDifferentDetails()){ // card details are different from the main details.
   			strCardHolder.append(woBean.getCcFirstName()).append(' ').append(woBean.getCcSurname());

			temp = woBean.getCcAddressLine1();
			if(temp != null && temp.length() > 0) strBillingAddress.append(temp);
			temp = woBean.getCcAddressLine2();
			if(temp != null && temp.length() > 0) strBillingAddress.append(',').append(temp);
			temp = woBean.getCcTown();
			if(temp != null && temp.length() > 0) strBillingAddress.append(',').append(temp);
			temp = woBean.getCcCounty();
			if(temp != null && temp.length() > 0) strBillingAddress.append(',').append(temp);

			strBillingPostCode = woBean.getCcPostcode();

		}else{
			strCardHolder.append(woBean.getDdFirstName()).append(' ').append(woBean.getDdSurname());
			strBillingAddress  = strDeliveryAddress;
			strBillingPostCode = strDeliveryPostCode;
		}




	 	// Cardholder value will be max of 50 characters.
		mapRequest.put("CardHolder"		, (strCardHolder.length() > 50 ? strCardHolder.substring(0,50) : strCardHolder.toString()));
		// same as cardholder name.
		mapRequest.put("CustomerName"	, (strCardHolder.length() > 50 ? strCardHolder.substring(0,50) : strCardHolder.toString()));
	  	mapRequest.put("CardNumber"		, woBean.getCreditCardNumber());
	  	mapRequest.put("StartDate"		, woBean.getStartDate());
	  	mapRequest.put("ExpiryDate"		, woBean.getExpiryDate());
	  	mapRequest.put("CardType"		, woBean.getCardType());
		mapRequest.put("CV2"			, woBean.getCardVerificationValue());
		mapRequest.put("IssueNumber"	, woBean.getIssueNumber());
		// Billing Address max 200 characters.
		mapRequest.put("BillingAddress"	, (strBillingAddress.length() > 200 ? strBillingAddress.substring(0,200) : strBillingAddress.toString()));
		// Billing PostCode max 10 characters.
		mapRequest.put("BillingPostCode", (strBillingPostCode.length() > 10 ? strBillingPostCode.substring(0,10) : strBillingPostCode.toString()));
		// delivery address Max 200 characters.
		mapRequest.put("DeliveryAddress",(strDeliveryAddress.length() > 200 ? strDeliveryAddress.substring(0,200) : strDeliveryAddress.toString()));
		// delivery postcode Max 10 characters.
		mapRequest.put("DeliveryPostCode",(strDeliveryPostCode.length() > 10 ? strDeliveryPostCode.substring(0,10) : strDeliveryPostCode.toString()));
		// telephone number Max 20 characters.
		mapRequest.put("ContactNumber",woBean.getTelephone());
		// Email address Max 255 characters.
		mapRequest.put("CustomerEMail",woBean.getEmailAddress());


		// Basket value has a upper limit of 7500 characters.
		// Remove all the occurances of the "&" character as the ProtX URL doesn't accept it.
		String strBasket = (sbBasket.length()>7500 ? sbBasket.substring(0,7500) : sbBasket.toString());
		strBasket = strBasket.replaceAll("&","AND");
		mapRequest.put("Basket"		, strBasket);

	  	// check whether this type of card requires a 3D Secure check or not.
		String cardTypes = resource.getProperty("cards3DSecureChecking");

		if(cardTypes != null && cardTypes.indexOf(woBean.getCardType())!= -1){
			// need to do 3D Secure check
			mapRequest.put("Apply3DSecure", FORCE_3D_SECURE_AUTHENTICATION);
		}else{
			// need NOT do check.
			mapRequest.put("Apply3DSecure", SKIP_3D_SECURE_AUTHENTICATION);
		}

    	String url = resource.getProperty("vpsURL");

		Map mapTXResponse = sendReceiveData(url,mapRequest);

		return mapTXResponse;

	}

	public static Map do3DSecureCallback(String MDValue,String PAResValue){
		Map mapRequest = new HashMap();
		mapRequest.put("MD",MDValue);
		mapRequest.put("PARes",PAResValue);

		ApplicationResources resource = ApplicationResources.getInstance();

		String url = resource.getProperty("vpsCallbackURL");
		Map mapTXResponse = sendReceiveData(url,mapRequest);

		return mapTXResponse;
	}

	/**
	 * Send/receive data to ProtX using low-level sockets.
	 */
	public static Map sendReceiveData(String url, Map mapRequest){
		Map mapTXResponse = null;
		try{

	    	trace("Request", mapRequest);

			// connect to the PROTX URL.
			URL ccurl = new URL(url);
			URLConnection connection = ccurl.openConnection();

			connection.setDoOutput(true);

			// send data
			PrintWriter writer = new PrintWriter(connection.getOutputStream());
			writer.print(formatData(mapRequest));
			writer.close();

			// read response
			BufferedReader reader =  new BufferedReader(new InputStreamReader(connection.getInputStream()));
			String line = null;
			mapTXResponse = new java.util.HashMap();

			while((line = reader.readLine()) != null){
				String [] pair = line.split( "=", 2 );
				if(pair.length == 2){
					mapTXResponse.put(pair[0],pair[1]);
				}
			}
			reader.close();

	    	trace("Response", mapTXResponse);

		}catch(Exception ex){
			ex.printStackTrace();
			throw new RuntimeException(ex);
		}

		return mapTXResponse;
	}

	/**
	 * Test implementation
	 */
	 public static Map doTransaction(){
		Map mapResponse = new HashMap();
		mapResponse.put("Status","OK");
		mapResponse.put("VPSTxId","34dr45rfdffg");
		mapResponse.put("SecurityKey","w34234sdsd");
		return mapResponse;
	 }

	/**
	 * Function to URL-encode the data to be sent.
	 *
	 * @param mapRequest
	 * 			Map containing the name-value pairs of the request parameters
	 * @return
	 * 			The url-encoded request string.
	 */
	private static String formatData(Map mapRequest)throws java.io.UnsupportedEncodingException{

		StringBuffer result = new StringBuffer();

		Iterator i = mapRequest.keySet().iterator();
		while(i.hasNext()){
			String key = (String)i.next();
			String encodedValue = URLEncoder.encode((String)mapRequest.get(key),"UTF-8");
			result.append(key + "=" + encodedValue);

			// separate each request parameter with the '&' character
			if(i.hasNext()){
				result.append('&');
			}
		}

		return result.toString();
	}

	private static void trace(Object message,Map map){
		System.out.println(message);
		Iterator i = map.keySet().iterator();
		while(i.hasNext()){
			String key = (String)i.next();
			System.out.println(key + "=" + map.get(key));
		}
	}
}